/*
 * Copyright 2010-2012 Susanta Tewari. <freecode4susant@users.sourceforge.net>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package coalescent.recursion;

import coalescent.EventType;
import coalescent.model.PopGenModel;
import coalescent.recursion.listener.exe.ExactRecursionExeEvent;
import coalescent.recursion.listener.exe.ExactRecursionExeListener;
import coalescent.statistic.AC;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;

import static coalescent.recursion.RecursionEventType.*;

/**
 * Recursion of the sample configuration backwards to its MRCA.
 *
 * @author Susanta Tewari
 * @version 1.0.0 Jun 14, 2010
 * @param <L>
 */
public class Recursion<L extends RecursionListener> {

    /** event listeners observing the backward recursion events */
    private final Collection<L> listeners = new HashSet<>(10);

    /** Field description */
    private final Collection<ExactRecursionExeListener> exeListeners = new ArrayList<>(10);

    /** Field description */
    private final Logger logger = Logger.getLogger(getClass().getName());

    /** Field description */
    private Map<Integer, Set<AC<?, ?>>> ancestralConfigCaches;

    /** to support computations based on the recursion run the last sample configuration */
    private final AC<?, ?> ac;

    /**
     * @param sampleConfig the sample configuration for which the recursion is run
     */
    public Recursion(final AC<?, ?> sampleConfig) {

        this.ac = sampleConfig;

        initCache();
    }

    /**
     * all the listeners are freed after the recursion is complete and before this method returns
     * @throws InterruptedException
     */
    public final void runRecursion() throws InterruptedException {

        if (listeners.isEmpty()) logger.warning("No computations for the recursion");

        fireEvent(new RecursionEvent(this, INIT_RECURSION, ac, null, null, null));
        startRecursion();
        fireEvent(new RecursionEvent(this, FINISHED_RECURSION, ac, null, null, null));
    }

    /**
     * @throws InterruptedException
     */
    protected void startRecursion() throws InterruptedException {
        recursion(ac);
    }

    /**
     * Implements the recursion
     *
     * @param sampleConfig allele spectrum
     * @throws InterruptedException
     */
    private void recursion(final AC<?, ?> sampleConfig) throws InterruptedException {

        if (sampleConfig.isMRCA()) {

            addToCache(sampleConfig);
            fireEvent(new RecursionEvent(this, BOUNDARY_CONDN, sampleConfig, null, null, null));

            return;
        }

        fireEvent(new RecursionEvent(this, STARTING_ITERATING_EVENTS, sampleConfig, null, null,
                                     null));

        for (final EventType type : getModel().getEventTypes()) {

            final Iterable<?> alleles = sampleConfig.alleles(type);

            fireEvent(new RecursionEvent(this, ITERATING_SINGLE_EVENT_TYPE, sampleConfig, null,
                                         null, type));

            for (final Object allele : alleles) {

                final AC<?, ?> ancestralConfig = sampleConfig.apply(allele, type);

                fireEvent(new RecursionEvent(this, PRE_VISIT_ANS_CONFIG, sampleConfig,
                                             ancestralConfig, allele, type));

                if (Thread.currentThread().isInterrupted()) {
                    throw new InterruptedException();
                }

                if (!isInCache(ancestralConfig)) recursion(ancestralConfig);

                fireEvent(new RecursionEvent(this, POST_VISIT_ANS_CONFIG, sampleConfig,
                                             ancestralConfig, allele, type));
            }

            fireEvent(new RecursionEvent(this, FINISHED_ITERATING_SINGLE_EVENT_TYPE, sampleConfig,
                                         null, null, type));
        }

        fireEvent(new RecursionEvent(this, FINISHED_ITERATING_EVENTS, sampleConfig, null, null,
                                     null));
        addToCache(sampleConfig);
        fireExeEvent();
    }

    /**
     * @return
     */
    public Map<Integer, Set<AC<?, ?>>> getAncestralConfigCaches() {
        return ancestralConfigCaches;
    }

    /**
     * @param aMember
     * @return
     */
    protected Set<AC<?, ?>> getAncestralCache(final AC<?, ?> aMember) {

        if (aMember.isEventsToMRCABounded()) {

            final Integer index = aMember.eventsToMRCA();

            return ancestralConfigCaches.get(index);

        } else {
            return ancestralConfigCaches.get(0);
        }
    }

    private void initCache() {

        int eventsToMRCA = 0;

        if (ac.isEventsToMRCABounded()) {
            eventsToMRCA = ac.eventsToMRCA();
        }

        ancestralConfigCaches = new ConcurrentHashMap<>(eventsToMRCA + 1);

        for (int i = 0; i < eventsToMRCA + 1; i++) {

            ancestralConfigCaches.put(i, Collections.newSetFromMap(new ConcurrentHashMap<AC<?, ?>,
                    Boolean>(10)));
        }
    }

    /**
     * @param s
     */
    protected void addToCache(final AC<?, ?> s) {

        if (s.isEventsToMRCABounded()) {

            final Integer index = s.eventsToMRCA();

            ancestralConfigCaches.get(index).add(s);

        } else {
            ancestralConfigCaches.get(0).add(s);
        }
    }

    /**
     * @param s
     * @return
     */
    protected boolean isInCache(final AC<?, ?> s) {

        if (s.isEventsToMRCABounded()) {

            final Integer index = s.eventsToMRCA();

            return ancestralConfigCaches.get(index).contains(s);

        } else {
            return ancestralConfigCaches.get(0).contains(s);
        }
    }


    // </editor-fold>

    /**
     * @return
     */
    public Collection<L> getListeners() {
        return listeners;
    }

    /**
     * @param event
     */
    protected final void fireEvent(final RecursionEvent event) {

        for (final RecursionListener listener : listeners) {

            switch (event.getRecursionEventType()) {

                case INIT_RECURSION :
                    listener.onInitRecursion(event);

                    break;

                case BOUNDARY_CONDN :
                    listener.onBoundaryCondn(event);

                    break;

                case STARTING_ITERATING_EVENTS :
                    listener.onStartingIteratingEvents(event);

                    break;

                case ITERATING_SINGLE_EVENT_TYPE :
                    listener.onIteratingSingleEventTypes(event);

                    break;

                case PRE_VISIT_ANS_CONFIG :
                    listener.onPreVisitAnsConfig(event);

                    break;

                case POST_VISIT_ANS_CONFIG :
                    listener.onPostVisitAnsConfig(event);

                    break;

                case FINISHED_ITERATING_SINGLE_EVENT_TYPE :
                    listener.onFinishedIteratingSingleEventTypes(event);

                    break;

                case FINISHED_ITERATING_EVENTS :
                    listener.onFinishedIteratingEvents(event);

                    break;

                case FINISHED_RECURSION :
                    listener.onFinishedRecursion(event);

                    break;

                default :
                    Logger.getLogger(this.getClass().getName()).log(
                        Level.WARNING,
                        "" + "Missed enum type in switch statement, needs immediate inspection.");
            }
        }
    }

    /**
     * @param listener
     */
    public void addExactRecursionEventListener(final L listener) {
        listeners.add(listener);
    }

    /**
     * @param listener
     */
    public void removeExactRecursionEventListener(final L listener) {
        listeners.remove(listener);
    }

    /**
     * @param listener
     */
    public void addExactRecursionExeListener(final ExactRecursionExeListener listener) {

        if (exeListeners.contains(listener)) {

            Logger.getLogger(getClass().getName()).log(Level.INFO,
                             "Listener {0} trying to register itself more than once: ", listener);

        } else {
            exeListeners.add(listener);
        }
    }

    /**
     * @param listener
     */
    public void removeExactRecursionExeListener(final ExactRecursionExeListener listener) {
        exeListeners.remove(listener);
    }

    /**
     */
    protected void fireExeEvent() {

        if (exeListeners.isEmpty()) return;


        // estimate cache size
        final List<Integer> cacheSize = getCacheSize();


        // collect the computation chunks
        final List<String> compChunks = new ArrayList<>(listeners.size());

        for (final RecursionListener listener : listeners) {

            if (listener.getUpdateChunk() != null) {
                compChunks.add(listener.getUpdateChunk());
            }
        }

        final ExactRecursionExeEvent event = new ExactRecursionExeEvent(this, cacheSize,
                                                 compChunks);

        for (final ExactRecursionExeListener listener : exeListeners) {
            listener.receivedExactRecursionExeEvent(event);
        }
    }

    protected List<Integer> getCacheSize() {

        final Integer[] cacheSize = new Integer[ancestralConfigCaches.size()];

        for (final Integer level : ancestralConfigCaches.keySet()) {
            cacheSize[level] = ancestralConfigCaches.get(level).size();
        }

        return Arrays.asList(cacheSize);
    }

    /**
     * @return
     */
    protected PopGenModel getModel() {
        return ac.getModel();
    }

    /**
     * @return
     */
    public AC<?, ?> getStatistic() {
        return ac;
    }
}
